package zack.project.aop;

import zack.project.types.annotation.DCCValue;
import zack.project.types.annotation.RateLimiterAccessInterceptor;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * @author A1793
 */
@Slf4j
@Aspect
@Component
public class RateLimiterAOP {

    //限流器开关
    @DCCValue("rateLimiterSwitch:open")
    private String rateLimiterSwitch;
    //用户的访问记录,用户在前端调用一次有RateLimiterAccessInterceptor注解的方法，就会使用该用户userId对应的限流器RateLimiter
    //该缓存1分钟失效一次，该限流器用来防止用户频繁调用某一个方法
    private final Cache<String, RateLimiter> logicRecord = CacheBuilder.newBuilder().
            expireAfterAccess(1, TimeUnit.MINUTES)
            .build();
    //用户黑名单
    private final Cache<String, Long> blcakList = CacheBuilder.newBuilder()
            .expireAfterAccess(24, TimeUnit.HOURS)
            .build();

    /**
     * 配置限流的切入点,拦截有@RataLimiterInterceptor注解的方法
     */
    @Pointcut("@annotation(zack.project.types.annotation.RateLimiterAccessInterceptor)")
    public void aopPoint() {
    }

    /**
     * 在有@RateLimiterAccessInterceptor注解的方法进行增强处理，在执行用户实际调用的方法之前进行增强
     *  @RateLimiterAccessInterceptor(key = "userId",
     *             fallbackMethod = "drawRateLimiterError",
     *             permitsPerSecond = 1.0d,
     *             blacklistCount = 1)
     * @param joinPoint
     * @param rateLimiterAccessInterceptor
     * @return
     * @throws Throwable
     */
    @Around("aopPoint()&& @annotation(rateLimiterAccessInterceptor)")
    public Object doRouter(ProceedingJoinPoint joinPoint,
                           RateLimiterAccessInterceptor rateLimiterAccessInterceptor) throws Throwable {
        //当限流开关配置为空或者配置为关闭时不做限流操作，该限流开关可以通过动态配置中心进行设置
        if (StringUtils.isBlank(rateLimiterSwitch) || "close".equals(rateLimiterSwitch)) {
            return joinPoint.proceed();
        }
        //获取注解里的值
        String key = rateLimiterAccessInterceptor.key();
        if (StringUtils.isBlank(key)) {
            throw new RuntimeException("annotation RateLimiter uId is null！");
        }
        //获取拦截字段,有@RateLimiterAccessInterceptor注解的方法的参数中应该有key的值配置
        //举例说明：RaffleActivityController.draw(ActivityDrawRequestDTO)方法，
        // 其参数ActivityDrawRequestDTO有对用的userId配置，下面的操作就是为了在前端传入的ActivityDrawRequestDTO获取对应的用户id
        String keyAttr = getAttrValue(key, joinPoint.getArgs());
        log.info("aop attr {}", keyAttr);

        //黑名单拦截,
        // 限流注解中配置的最高访问次数非0且该用户缓存中的超过最高访问次数的次数超过限流配置的加入黑名单次数时，就直接调用降级方法
        if (!"all".equals(keyAttr)
                && rateLimiterAccessInterceptor.blacklistCount() != 0
                && blcakList.getIfPresent(keyAttr) != null
                && blcakList.getIfPresent(keyAttr) > rateLimiterAccessInterceptor.blacklistCount()) {

            log.info("限流-黑名单拦截(24h)：{}", keyAttr);
            return fallbackMethodResult(joinPoint, rateLimiterAccessInterceptor.fallbackMethod());
        }

        //没有被黑名单拦截，则根据用户id获取限流器，如果没有则根据注解配置新建一个并存入缓存logicRecord
        RateLimiter rateLimiter = logicRecord.getIfPresent(keyAttr);
        if (rateLimiter == null) {
            rateLimiter = rateLimiter.create(rateLimiterAccessInterceptor.permitsPerSecond());
            logicRecord.put(keyAttr, rateLimiter);
        }
        //限流进行拦截，限流器进行拦截时会根据permitsPerSecond(每秒最高可访问次数)来进行判断tryAcquire的结果
        //限流器具有存储用户每秒的访问次数，当访问次数超限时，tryAcquire会返回false
        if (!rateLimiter.tryAcquire()) {
            //如果是第一次每秒访问超限时，会在黑名单存储新建一个entry，
            //存储该用户的被限流次数，当超限次数超过配置的允许次数(blacklistCount)时会直接被黑名单拦截进行降级处理
            if (rateLimiterAccessInterceptor.blacklistCount() != 0) {
                if (blcakList.getIfPresent(keyAttr) == null) {
                    blcakList.put(keyAttr, 1L);
                } else {
                    //如果该用户对应的被限流次数存在，则在原基础次数上+1
                    blcakList.put(keyAttr, blcakList.getIfPresent(keyAttr) + 1L);
                }
            }
            log.info("限流-超频次拦截：{}", keyAttr);
            //每次每秒访问次数超限次数时都会进行降级处理
            return fallbackMethodResult(joinPoint, rateLimiterAccessInterceptor.fallbackMethod());
        }
        //未被黑名单拦截且每秒访问没有超限时正常调用方法
        return joinPoint.proceed();

    }

    public Object fallbackMethodResult(ProceedingJoinPoint joinPoint, String fallbackMethod) throws Throwable {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;

        Method method = joinPoint.getTarget().getClass().getMethod(fallbackMethod,
                methodSignature.getParameterTypes());

        return method.invoke(joinPoint.getThis(), joinPoint.getArgs());
    }

    /**
     * 根据属性名来获取属性值
     * @param attr 属性名
     * @param args 原方法的参数列表
     * @return
     */
    public String getAttrValue(String attr, Object[] args) {
        if (args[0] instanceof String) {
            return args[0].toString();
        }
        String filedValue = null;
        for (Object arg : args) {
            try {
                if (StringUtils.isNotBlank(filedValue)) {
                    break;
                }
                // filedValue = BeanUtils.getProperty(arg, attr);
                // fix: 使用lombok时，uId这种字段的get方法与idea生成的get方法不同，会导致获取不到属性值，改成反射获取解决
                filedValue = String.valueOf(this.getValueByName(arg, attr));
            } catch (Exception e) {
                log.error("获取路由属性值失败 attr：{}", attr, e);
            }
        }
        return filedValue;
    }

    /**
     * 获取对象的特定属性值
     *
     * @param item 对象
     * @param name 属性名
     * @return 属性值
     * @author tang
     */
    private Object getValueByName(Object item, String name) {
        try {
            //根据属性名在对象中的对应的字段
            Field field = getFieldByName(item, name);
            if (field == null) {
                return null;
            }
            field.setAccessible(true);
            //在对象中获得对应属性的值
            Object o = field.get(item);
            field.setAccessible(false);
            return o;
        } catch (IllegalAccessException e) {
            return null;
        }
    }

    /**
     * 根据名称获取方法，该方法同时兼顾继承类获取父类的属性
     *
     * @param item 对象
     * @param name 属性名
     * @return 该属性对应方法
     * @author tang
     */
    private Field getFieldByName(Object item, String name) {
        try {
            Field field;
            try {
                field = item.getClass().getDeclaredField(name);
            } catch (NoSuchFieldException e) {
                field = item.getClass().getSuperclass().getDeclaredField(name);
            }
            return field;
        } catch (NoSuchFieldException e) {
            return null;
        }
    }

}
